package ch.usi.inf.pf2.alu;

import java.util.ArrayList;

import ch.usi.inf.pf2.Sheet;
import ch.usi.inf.pf2.alu.nodes.*;
import ch.usi.inf.pf2.analyzer.Analyzer;
import ch.usi.inf.pf2.analyzer.Token;
import ch.usi.inf.pf2.analyzer.TokenType;
import ch.usi.inf.pf2.analyzer.UnrecognizedTokenException;
import ch.usi.inf.pf2.cell.Converter;
import ch.usi.inf.pf2.cell.Data;
import ch.usi.inf.pf2.cell.Formula;
import ch.usi.inf.pf2.cell.Value;

/**
 * Parse formula.
 * A Formula cannot contain an operator followed by another one without having parenthesis between them: 
 * -3 + (-3) -> OK
 * -3 + -3 -> NOT OK 
 * 
 * 
 * @author Thomas Mantegazzi, Yotam Sharon
 * @date 23 april 2010
 *
 */

public class Parser {

	private Sheet sheet;
	private Analyzer analyzer;
	private ArrayList<String> cellsAlreadyVisited;

	public Parser (Sheet sheet) {
		this.sheet = sheet;
		this.analyzer = new Analyzer();
		cellsAlreadyVisited = new ArrayList<String>();
	}

	/**
	 * Add a Token to a Token[] at the last position
	 * @param tokens The Token[] where to append the new Token
	 * @param t The new Token to append
	 * @return the new Token[] with the new Token added
	 */
	private Token[] addToken(Token[] tokens, Token t) {
		Token[] newTokens = new Token[tokens.length + 1];
		for (int i = 0; i < tokens.length; i++) {
			newTokens[i] = tokens[i];
		}
		newTokens[tokens.length] = t;
		return newTokens;
	}

	/**
	 * Pass through the expression, tokenize it and store a Token[] 
	 * in tokens with all the Token found in expression
	 */
	public Token[] analyze(String expression) throws UnrecognizedTokenException, ParenthesisException, UnrecognizedIdentifierException {
		Token[] tokens = new Token[0];
		int parenthesisCounter = 0;
		analyzer.setText(expression);
		while (true) {
			analyzer.fetchNextToken();
			if (analyzer.getCurrentToken().getType() == TokenType.END_OF_TEXT){
				if (parenthesisCounter > 0) {
					throw new ParenthesisException("Error: missing " + parenthesisCounter + " ')'");
				}
				if (parenthesisCounter < 0) {
					throw new ParenthesisException("Error: missing " + -parenthesisCounter + " '('" );
				}
				return tokens;
			}
			tokens = addToken(tokens, analyzer.getCurrentToken());
			// check the number of parenthesis, if it is not even, throw ParenthesisException
			if (analyzer.getCurrentToken().getType() == TokenType.OPEN_PARENTHESIS) {
				parenthesisCounter++;
			} else if (analyzer.getCurrentToken().getType() == TokenType.CLOSED_PARENTHESIS) {
				parenthesisCounter--;
			}
		}
	}

	/**
	 * Return the left part of a Token[], from position 0 to index
	 * @param tokens The Token[] from which to take the left part
	 * @param index The index where to stop coping the content of tokens
	 * @return The left part of tokens, from position 0 to index
	 */
	public Token[] takeLeft(Token[] tokens, int index){
		Token[] leftTokens = new Token[index];
		for (int i = 0; i < index; i++) {
			leftTokens[i] = tokens[i];
		}
		return leftTokens;
	}

	/**
	 * Return the right part of a Token[], from position (index + 1) to the end of the array
	 * @param tokens The Token[] from which to take the right part
	 * @param index The index where to start coping the content of tokens
	 * @return The right part of tokens, from position (index + 1) to the end of the array
	 */
	public Token[] takeRight(Token[] tokens, int index){
		Token[] rightTokens = new Token[tokens.length - index - 1];
		for (int i = index + 1; i < tokens.length; i++) {
			rightTokens[i - (index + 1)] = tokens[i];
		}
		return rightTokens;
	}

	/**
	 * Remove the first and last elements of a Token[], which are parenthesis
	 * @param tokens The Token[] from which to remove the parenthesis
	 * @return the new Token[] without the first and last elements
	 */
	public Token[] removeParenthesis(Token[] tokens) throws MissingParenthesisException {
		if (tokens.length < 3) {
			throw new MissingParenthesisException("Error: missing parenthesis '(..)'"); 
		}
		Token[] newTokens = new Token[tokens.length - 2];
		for (int i = 1; i < tokens.length - 1; i++) {
			newTokens[i - 1] = tokens[i];
		}
		return newTokens;
	}

	private Node[] addNode(Node n, Node[] nodes) {
		Node[] newNodes = new Node[nodes.length +1];
		for(int i = 0; i < nodes.length; i++) {
			newNodes[i] = nodes[i];
		}
		newNodes[nodes.length] = n;
		return newNodes;
	}

	private Token[] addToken(Token n, Token[] tokens) {
		Token[] newTokens = new Token[tokens.length +1];
		for(int i = 0; i < tokens.length; i++) {
			//printTokenArray(newTokens);
			newTokens[i] = tokens[i];
		}
		newTokens[tokens.length] = n;
		return newTokens;
	}

	/*private void printTokenArray(Token[] tokens) {
		System.out.println("Token Array Length: "+tokens.length);
		for(Token t : tokens) {
			System.out.print(t.getText()+" ");
		}
		System.out.println();
		System.out.println();
	}*/

	/**
	 * Add an int[] to an int[][]
	 */
	private int[][] addIntArrayToIntArrayArray(int[][] intArrayArray, int[] intArray) {
		int[][] newIntArrayArray = new int[intArrayArray.length + 1][];
		for (int i = 0; i<intArrayArray.length; i++) {
			newIntArrayArray[i] = intArrayArray[i];
		}
		newIntArrayArray[intArrayArray.length] = intArray;
		return newIntArrayArray;
	}


	/**
	 * Give an array with all cell references from cellRef1 to cellRef2
	 * @return An array of int[] containg all cell references
	 */
	private int[][] getListOfCellReferences(int[] cellRef1, int[] cellRef2) {
		int letterMin = 0;
		int letterMax = 0;
		int numberMin = 0;
		int numberMax = 0;
		if (cellRef1[0] > cellRef2[0]) {
			letterMax = cellRef1[0];
			letterMin = cellRef2[0];
		} else {
			letterMin = cellRef1[0];
			letterMax = cellRef2[0];
		}
		if (cellRef1[1] > cellRef2[1]) {
			numberMax = cellRef1[1];
			numberMin = cellRef2[1];
		} else {
			numberMin = cellRef1[1];
			numberMax = cellRef2[1];
		}
		// creates the int[][]
		int[][] list = new int[0][];
		for (int i = letterMin; i <= letterMax; i++) {
			for (int j = numberMin; j <= numberMax; j++) {
				int[] newIntArray = new int[]{i,j};
				list = addIntArrayToIntArrayArray(list, newIntArray);
			}
		}
		return list;
	}

	/**
	 * Given a Token[] containing 2 cell References, return a Token[] with all cell references between them
	 * @param tokens The Token[] containing the 2 cell references
	 * @return The Token[] containing all cell references
	 */
	private Token[] parseListOfCellReferences(Token[] tokens) {
		//System.out.println(""+tokens.length);
		Converter c = sheet.getConverter();
		c.setCellReference(tokens[0].getText());
		c.convertToNumbers();
		int[] cellRef1 = c.getCellReferenceNumbers();
		c.setCellReference(tokens[2].getText());
		c.convertToNumbers();
		int[] cellRef2 = c.getCellReferenceNumbers();
		int[][] allCellRef = getListOfCellReferences(cellRef1, cellRef2);
		//System.out.println(""+allCellRef.length);
		// convert all int[] to String (cell references)
		Token[] newTokens = new Token[2*allCellRef.length -1];
		for (int i = 0; i < allCellRef.length; i++) {
			c.setCellReferenceNumbers(allCellRef[i]);
			c.convertToString();
			newTokens[2*i] = new Token(c.getCellReference(), 2*i, TokenType.CELL_REFERENCE);
			if (i != allCellRef.length - 1) {
				newTokens[2*i+1] = new Token(",", 2*i+1, TokenType.COMMA);
			}
		}
		return newTokens;
	}

	/**
	 * copies the contents of the given array in a new array
	 * @return The new array containing the same elements as the given array
	 */
	private ArrayList<String> copyArray(ArrayList<String> arrayToCopy) {
		ArrayList<String> arrayToReturn = new ArrayList<String>();
		for (int i = 0; i < arrayToCopy.size(); i++) {
			arrayToReturn.add(arrayToCopy.get(i));
		}
		return arrayToReturn;
	}

	/**
	 * Parse identifiers
	 */
	private Node[] getNodeArray(Token[] tokens) throws UnrecognizedTokenException, ParenthesisException, UnrecognizedIdentifierException, SyntaxException, WrongNumberOfArgumentsException, CircularReferenceException, MissingParenthesisException, ReferenceToFormulaWithErrorException {
		ArrayList<String> cellsVisitedBefore = copyArray(cellsAlreadyVisited);

		Token[] newTokenArray = new Token[0];
		Node[] newNodeArray = new Node[0];
		int parenthesisCounter = 0;
		for(int i = 0; i < tokens.length; i++){
			cellsAlreadyVisited.clear();
			if (tokens[i].getType() == TokenType.OPEN_PARENTHESIS) {
				newTokenArray = addToken(tokens[i],newTokenArray);
				parenthesisCounter++;
			} else if (tokens[i].getType() == TokenType.CLOSED_PARENTHESIS) {
				newTokenArray = addToken(tokens[i],newTokenArray);
				parenthesisCounter--;
			} else if (tokens[i].getType() == TokenType.COMMA && parenthesisCounter == 0) {
				if (containsColon(newTokenArray)) {
					newNodeArray = addNodeArrayToNodeArray(newNodeArray, getNodeArray(parseListOfCellReferences(newTokenArray)));
				} else {
					newNodeArray = addNode(parse(newTokenArray), newNodeArray);
				}
				newTokenArray = new Token[0];
			} else {
				newTokenArray = addToken(tokens[i],newTokenArray);
			}
		}
		if (containsColon(newTokenArray)) {
			//System.out.println("SI");
			newNodeArray = addNodeArrayToNodeArray(newNodeArray, getNodeArray(parseListOfCellReferences(newTokenArray)));
		} else {
			newNodeArray = addNode(parse(newTokenArray), newNodeArray);
		}
		cellsAlreadyVisited.clear();
		cellsAlreadyVisited = cellsVisitedBefore;
		return newNodeArray;
	}

	/**
	 * Check if the Token[] contains a COLON
	 * @param tokens The Token[] to be checked
	 * @return true or false
	 */
	private boolean containsColon(Token[] tokens) {
		for (int i = 0; i<tokens.length; i++) {
			if (tokens[i].getType() == TokenType.COLON) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Add a Node[] to other Node[]
	 * @param nodeArray1 The Node[] to extend
	 * @param nodeArray2 The Node[] to add
	 * @return The extended Node[]
	 */
	private Node[] addNodeArrayToNodeArray(Node[] nodeArray1, Node[] nodeArray2) {
		Node[] newNodeArray = new Node[nodeArray1.length + nodeArray2.length];
		for(int i = 0; i < nodeArray1.length; i++) {
			newNodeArray[i] = nodeArray1[i];
		}
		for(int j = 0; j < nodeArray2.length; j++) {
			newNodeArray[j + nodeArray1.length] = nodeArray2[j];
		}
		return newNodeArray;
	}


	/**
	 * Create a tree from a Token[]
	 * @param tokens The Token[] to be parsed
	 * @return a root Node
	 */
	public Node parse(Token[] tokens) throws UnrecognizedTokenException, ParenthesisException, UnrecognizedIdentifierException, SyntaxException, WrongNumberOfArgumentsException, CircularReferenceException, MissingParenthesisException, ReferenceToFormulaWithErrorException {
		if (tokens.length == 0) {
			//return null;
			throw new SyntaxException("Error: syntax error");
		}

		if(tokens[0].getType() == TokenType.OPEN_PARENTHESIS && tokens[tokens.length-1].getType() == TokenType.CLOSED_PARENTHESIS){
			tokens = removeParenthesis(tokens);
			return parse(tokens);
		}

		int parenthesisCounter = 0;

		//Handling Single Token: FLOAT_LITERAL or CELL_REFERENCE
		if(tokens.length == 1){
			//Handle CELL_REFERENCE
			if(tokens[0].getType() == TokenType.CELL_REFERENCE) {
				if (cellsAlreadyVisited.contains(tokens[0].getText())) {
					throw new CircularReferenceException("Error: circular reference");
				}
				cellsAlreadyVisited.add(tokens[0].getText());
				Data cell = sheet.getCellByName(tokens[0].getText());
				if(cell instanceof Value) {
					return new NumberLiteralNode(((Value) cell).getInstanceDouble());
				} else if(cell instanceof Formula) {
					if (((Formula) cell).isComputed()) {
						if (!((Formula)cell).isThereError() || ((Formula)cell).getErrorMessage().equalsIgnoreCase("Missing values in references")) {
							return new NumberLiteralNode(Double.parseDouble(((Formula)cell).getActualValue()));	
						} else {
							throw new ReferenceToFormulaWithErrorException("Error: reference to formula containing errors");
						}
					} else {
						Node tempNode = parse(analyze(((Formula) cell).getInstanceWithoutEqualSign()));
						return setActualValueOfFormula(tempNode, tokens[0].getText());
					}
				} else {
					return new ZeroNode();
				}

			} else
				//Handle FLOAT_LITERAL
				if(tokens[0].getType() == TokenType.FLOAT_LITERAL) {
					return new NumberLiteralNode(Double.parseDouble(tokens[0].getText()));
				}
		}

		//Jump Parenthesis
		for(int index = tokens.length - 1; index >= 0;index--){
			if(tokens[index].getType() == TokenType.OPEN_PARENTHESIS || tokens[index].getType() == TokenType.CLOSED_PARENTHESIS){
				if(tokens[index].getType() == TokenType.OPEN_PARENTHESIS){
					parenthesisCounter++;
				}
				if(tokens[index].getType() == TokenType.CLOSED_PARENTHESIS){
					parenthesisCounter--;
				}
			}else{
				if(parenthesisCounter !=0){

				}else{

					//Handle Plus and Minus Operators
					if(tokens[index].getType() == TokenType.PLUS || tokens[index].getType() == TokenType.MINUS){
						Token[] leftTokens; 
						if(takeLeft(tokens,index).length == 0) {
							leftTokens = new Token[] {new Token("0", index , TokenType.FLOAT_LITERAL)};
						}else {
							leftTokens = takeLeft(tokens,index);
						}
						if(tokens[index].getType() == TokenType.PLUS) {
							return new AdditionNode(parse(leftTokens),parse(takeRight(tokens,index)));
						} else if(tokens[index].getType() == TokenType.MINUS) {
							return new SubtractionNode(parse(leftTokens),parse(takeRight(tokens,index)));
						}
					}else{}
				}
			}
		}
		for(int index = 0; index<tokens.length;index++){

			//Check for parenthesis
			if(tokens[index].getType() == TokenType.OPEN_PARENTHESIS || tokens[index].getType() == TokenType.CLOSED_PARENTHESIS){
				if(tokens[index].getType() == TokenType.OPEN_PARENTHESIS){
					parenthesisCounter++;
				}
				if(tokens[index].getType() == TokenType.CLOSED_PARENTHESIS){
					parenthesisCounter--;
				}
			}else{
				if(parenthesisCounter !=0){}else{

					//Handle Star, Slash and Modulo Operators
					if(tokens[index].getType() == TokenType.STAR || tokens[index].getType() == TokenType.SLASH || tokens[index].getType() == TokenType.MODULO){
						if(tokens[index].getType() == TokenType.STAR) {
							return new MultiplicationNode(parse(takeLeft(tokens,index)),parse(takeRight(tokens,index)));
						} else if(tokens[index].getType() == TokenType.SLASH) {
							return new DivisionNode(parse(takeLeft(tokens,index)),parse(takeRight(tokens,index)));
						} else if(tokens[index].getType() == TokenType.MODULO) {
							return new ModuloNode(parse(takeLeft(tokens,index)),parse(takeRight(tokens,index)));
						}
					}else{}
				}
			}
		}
		for(int index = 0; index<tokens.length;index++){

			//Check for parenthesis
			if(tokens[index].getType() == TokenType.OPEN_PARENTHESIS || tokens[index].getType() == TokenType.CLOSED_PARENTHESIS){
				if(tokens[index].getType() == TokenType.OPEN_PARENTHESIS){
					parenthesisCounter++;
				}
				if(tokens[index].getType() == TokenType.CLOSED_PARENTHESIS){
					parenthesisCounter--;
				}
			}else{
				if(parenthesisCounter !=0){}else{

					//Handle Power Operator
					if(tokens[index].getType() == TokenType.POWER){
						return new PowerNode(parse(takeLeft(tokens,index)),parse(takeRight(tokens,index)));
					}else{}
				}
			}
		}
		//Handle Identifiers
		if(tokens[0].getType() == TokenType.IDENTIFIER) {
			String identifier = tokens[0].getText().toLowerCase();
			if(identifier.equals("sum")) {
				return new SumNode(getNodeArray(removeParenthesis(takeRight(tokens,0))));
			} else if(identifier.equals("max")) {
				return new MaxNode(getNodeArray(removeParenthesis(takeRight(tokens,0))));
			} else if(identifier.equals("min")) {
				return new MinNode(getNodeArray(removeParenthesis(takeRight(tokens,0))));
			} else if(identifier.equals("average")) {
				return new AverageNode(getNodeArray(removeParenthesis(takeRight(tokens,0))));
			} else if(identifier.equals("cos")) {
				return new CosNode(getNodeArray(removeParenthesis(takeRight(tokens,0))));
			} else if(identifier.equals("cot")) {
				return new CotNode(getNodeArray(removeParenthesis(takeRight(tokens,0))));
			} else if(identifier.equals("sin")) {
				return new SinNode(getNodeArray(removeParenthesis(takeRight(tokens,0))));
			} else if(identifier.equals("ln")) {
				return new LnNode(getNodeArray(removeParenthesis(takeRight(tokens,0))));
			} else if(identifier.equals("tan")) {
				return new TanNode(getNodeArray(removeParenthesis(takeRight(tokens,0))));
			} else if(identifier.equals("log")) {
				return new LogNode(getNodeArray(removeParenthesis(takeRight(tokens,0))));
			}
		}
		throw new  SyntaxException("Error: syntax error");
	}

	private Node setActualValueOfFormula(Node tempNode, String text) {
		Value value = (Value)tempNode.evaluate();
		Formula f = (Formula)sheet.getCellByName(text);
		f.setActualValue(value.getInstance());
		f.setComputed(true);
		return tempNode;
	}

	public Node parseFormula(String expression, String cellReference) throws UnrecognizedTokenException, ParenthesisException, UnrecognizedIdentifierException, SyntaxException, WrongNumberOfArgumentsException, CircularReferenceException, MissingParenthesisException, ReferenceToFormulaWithErrorException {
		cellsAlreadyVisited.clear();
		cellsAlreadyVisited.add(cellReference);
		Node tree = parse(analyze(expression));
		return tree;
	}

}